Add regexp matching support to EventFormattingAgent.

A new setting key `matchers` is added to define regular expression
matching against contents of events and expand the match data for use
in `instructions` setting.

Akinori MUSHA 11 years ago
parent
commit
030c7d685a
2 changed files with 140 additions and 7 deletions
  1. 121 3
      app/models/agents/event_formatting_agent.rb
  2. 19 4
      spec/models/agents/event_formatting_agent_spec.rb

+ 121 - 3
app/models/agents/event_formatting_agent.rb

@@ -12,6 +12,10 @@ module Agents
12 12
               "celsius": "18",
13 13
               "fahreinheit": "64"
14 14
             },
15
+            "date": {
16
+              "epoch": "1357959600",
17
+              "pretty": "10:00 PM EST on January 11, 2013"
18
+            },
15 19
             "conditions": "Rain showers",
16 20
             "data": "This is some data"
17 21
           }
@@ -33,6 +37,33 @@ module Agents
33 37
             "subject": "This is some data"
34 38
           }
35 39
 
40
+      In `matchers` setting you can perform regular expression matching against contents of events and expand the match data for use in `instructions` setting.  Here is an example:
41
+
42
+          {
43
+            "matchers": [
44
+              {
45
+                "path": "$.date.pretty",
46
+                "regexp": "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)",
47
+                "to": "pretty_date",
48
+              }
49
+            ]
50
+          }
51
+
52
+      This virtually merges the following hash into the original event hash:
53
+
54
+          "pretty_date": {
55
+            "time": "10:00 PM EST",
56
+            "0": "10:00 PM EST on January 11, 2013"
57
+            "1": "10:00 PM EST",
58
+          }
59
+
60
+      So you can use it in `instructions` like this:
61
+
62
+          "instructions": {
63
+            "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius according to the forecast at <$.pretty_date.time>.",
64
+            "subject": "$.data"
65
+          }
66
+
36 67
       If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`.
37 68
 
38 69
       By default, the output event will have `agent` and `created_at` fields added as well, reflecting the original Agent type and Event creation time.  You can skip these outputs by setting `skip_agent` and `skip_created_at` to `true`.
@@ -46,8 +77,48 @@ module Agents
46 77
 
47 78
     event_description "User defined"
48 79
 
80
+    after_save :clear_matchers
81
+
49 82
     def validate_options
50 83
       errors.add(:base, "instructions, mode, skip_agent, and skip_created_at all need to be present.") unless options['instructions'].present? and options['mode'].present? and options['skip_agent'].present? and options['skip_created_at'].present?
84
+
85
+      case matchers = options['matchers']
86
+      when nil
87
+      when Array
88
+        matchers.each do |matcher|
89
+          unless Hash === matcher
90
+            errors.add(:base, "each matcher must be a hash")
91
+            next
92
+          end
93
+
94
+          regexp, path, to = matcher.values_at(*%w[regexp path to])
95
+
96
+          case regexp
97
+          when String
98
+            begin
99
+              Regexp.new(regexp)
100
+            rescue
101
+              errors.add(:base, "bad regexp found in matchers: #{regexp}")
102
+            end
103
+          else
104
+            errors.add(:base, "regexp is mandatory for a matcher and must be a string")
105
+          end
106
+
107
+          case path
108
+          when String
109
+          else
110
+            errors.add(:base, "path is mandatory for a matcher and must be a string")
111
+          end
112
+
113
+          case to
114
+          when nil, String
115
+          else
116
+            errors.add(:base, "to must be a string if present in a matcher")
117
+          end
118
+        end
119
+      else
120
+        errors.add(:base, "matchers must be an array if present")
121
+      end
51 122
     end
52 123
 
53 124
     def default_options
@@ -56,6 +127,7 @@ module Agents
56 127
           'message' =>  "You received a text <$.text> from <$.fields.from>",
57 128
           'some_other_field' => "Looks like the weather is going to be <$.fields.weather>"
58 129
         },
130
+        'matchers' => [],
59 131
         'mode' => "clean",
60 132
         'skip_agent' => "false",
61 133
         'skip_created_at' => "false"
@@ -68,12 +140,58 @@ module Agents
68 140
 
69 141
     def receive(incoming_events)
70 142
       incoming_events.each do |event|
71
-        formatted_event = options['mode'].to_s == "merge" ? event.payload : {}
72
-        options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, event.payload) }
143
+        formatted_event = options['mode'].to_s == "merge" ? event.payload.dup : {}
144
+        payload = perform_matching(event.payload)
145
+        options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, payload) }
73 146
         formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless options['skip_agent'].to_s == "true"
74 147
         formatted_event['created_at'] = event.created_at unless options['skip_created_at'].to_s == "true"
75 148
         create_event :payload => formatted_event
76 149
       end
77 150
     end
151
+
152
+    def perform_matching(payload)
153
+      matchers.inject(payload.dup) { |hash, matcher|
154
+        matcher[hash]
155
+      }
156
+    end
157
+
158
+    def matchers
159
+      @matchers ||=
160
+        if matchers = options['matchers']
161
+          matchers.map { |matcher|
162
+            regexp, path, to = matcher.values_at(*%w[regexp path to])
163
+            re = Regexp.new(regexp)
164
+            proc { |hash|
165
+              mhash = {}
166
+              value = Utils.value_at(hash, path)
167
+              if String === value and m = re.match(value)
168
+                m.to_a.each_with_index { |s, i|
169
+                  mhash[i.to_s] = s
170
+                }
171
+                m.names.each do |name|
172
+                  mhash[name] = m[name]
173
+                end if m.respond_to?(:names)
174
+              end
175
+              if to
176
+                case value = hash[to]
177
+                when Hash
178
+                  value.update(mhash)
179
+                else
180
+                  hash[to] = mhash
181
+                end
182
+              else
183
+                hash.update(mhash)
184
+              end
185
+              hash
186
+            }
187
+          }
188
+        else
189
+          []
190
+        end
191
+    end
192
+
193
+    def clear_matchers
194
+      @matchers = nil
195
+    end
78 196
   end
79
-end
197
+end

+ 19 - 4
spec/models/agents/event_formatting_agent_spec.rb

@@ -7,9 +7,16 @@ describe Agents::EventFormattingAgent do
7 7
         :options => {
8 8
             :instructions => {
9 9
                 :message => "Received <$.content.text.*> from <$.content.name> .",
10
-                :subject => "Weather looks like <$.conditions>"
10
+                :subject => "Weather looks like <$.conditions> according to the forecast at <$.pretty_date.time>"
11 11
             },
12 12
             :mode => "clean",
13
+            :matchers => [
14
+                {
15
+                    :path => "$.date.pretty",
16
+                    :regexp => "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)",
17
+                    :to => "pretty_date",
18
+                },
19
+            ],
13 20
             :skip_agent => "false",
14 21
             :skip_created_at => "false"
15 22
         }
@@ -24,7 +31,11 @@ describe Agents::EventFormattingAgent do
24 31
     @event.payload = {
25 32
         :content => {
26 33
             :text => "Some Lorem Ipsum",
27
-            :name => "somevalue"
34
+            :name => "somevalue",
35
+        },
36
+        :date => {
37
+            :epoch => "1357959600",
38
+            :pretty => "10:00 PM EST on January 11, 2013"
28 39
         },
29 40
         :conditions => "someothervalue"
30 41
     }
@@ -61,7 +72,11 @@ describe Agents::EventFormattingAgent do
61 72
     it "should handle JSONPaths in instructions" do
62 73
       @checker.receive([@event])
63 74
       Event.last.payload[:message].should == "Received Some Lorem Ipsum from somevalue ."
64
-      Event.last.payload[:subject].should == "Weather looks like someothervalue"
75
+    end
76
+
77
+    it "should handle matchers and JSONPaths in instructions" do
78
+      @checker.receive([@event])
79
+      Event.last.payload[:subject].should == "Weather looks like someothervalue according to the forecast at 10:00 PM EST"
65 80
     end
66 81
 
67 82
     it "should allow escaping" do
@@ -125,4 +140,4 @@ describe Agents::EventFormattingAgent do
125 140
       @checker.should_not be_valid
126 141
     end
127 142
   end
128
-end
143
+end